// Copyright (c) 2008 Davy Brion (ralinx@davybrion.com)
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
// 
//     * Redistributions of source code must retain the above copyright notice,
//       this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright notice,
//       this list of conditions and the following disclaimer in the documentation
//       and/or other materials provided with the distribution.
//     * Neither the name of Davy Brion nor the names of the
//       contributors may be used to endorse or promote products derived from this
//       software without specific prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Albatross.Common.WCF
{
	public static class KnownTypeProvider
	{
		private static HashSet<Type> knownTypes = new HashSet<Type>();
		private static Dictionary<Type, HashSet<Type>> knownTypesByBaseType = new Dictionary<Type, HashSet<Type>>();
		private static Dictionary<string, HashSet<Type>> knownTypesByMethod = new Dictionary<string, HashSet<Type>>();

		public static void ClearAllKnownTypes()
		{
			knownTypes = new HashSet<Type>();
			knownTypesByBaseType = new Dictionary<Type, HashSet<Type>>();
			knownTypesByMethod = new Dictionary<string, HashSet<Type>>();
		}

		public static void RegisterDerivedTypesOf<T>(Assembly assembly)
		{
			RegisterDerivedTypesOf<T>(assembly.GetTypes());
		}

		public static void RegisterDerivedTypesOf<T>(IEnumerable<Type> types)
		{
			List<Type> derivedTypes = GetDerivedTypesOf<T>(types);
			knownTypes.UnionWith(derivedTypes);
			UpdateKnownTypesByBaseType<T>(derivedTypes);
		}

		public static IEnumerable<Type> GetKnownTypes(ICustomAttributeProvider provider)
		{
			var methodInfo = provider as MethodInfo;

			if (methodInfo == null)
			{
				// if the provider is not a MethodInfo instance for some reason, just return all known types
				return knownTypes;
			}

			string key = GetMethodInfoKey(methodInfo);

			if (!knownTypesByMethod.ContainsKey(key))
			{
				knownTypesByMethod[key] = GetKnownTypesFor(methodInfo);
			}

			return knownTypesByMethod[key];
		}

		public static IEnumerable<Type> GetKnownTypesOf<T>()
		{
			return GetKnownTypesOf(typeof(T));
		}

		public static IEnumerable<Type> GetKnownTypesOf(Type baseType)
		{
			if (!knownTypesByBaseType.ContainsKey(baseType))
			{
				// we don't have any known types for this base type, so just return an empty array
				return new Type[0];
			}

			return knownTypesByBaseType[baseType];
		}

		private static HashSet<Type> GetKnownTypesFor(MethodInfo methodInfo)
		{
			IEnumerable<Type> foundTypes = GetAllParameterAndReturnTypesFrom(methodInfo);

			var relatedKnownTypes = new HashSet<Type>();

			foreach (Type type in foundTypes)
			{
				relatedKnownTypes.UnionWith(GetKnownTypesOf(type));
			}
			return relatedKnownTypes;
		}

		private static IEnumerable<Type> GetAllParameterAndReturnTypesFrom(MethodInfo methodInfo)
		{
			var types = new List<Type>();

			foreach (ParameterInfo parameter in methodInfo.GetParameters())
			{
				types.Add(GetTypeOrElementType(parameter.ParameterType));
			}

			if (methodInfo.ReturnType != typeof(void))
			{
				types.Add(GetTypeOrElementType(methodInfo.ReturnType));
			}

			return types;
		}

		private static Type GetTypeOrElementType(Type type)
		{
			return type.IsArray ? type.GetElementType() : type;
		}

		private static void UpdateKnownTypesByBaseType<T>(IEnumerable<Type> derivedTypes)
		{
			Type baseType = typeof(T);

			if (knownTypesByBaseType.ContainsKey(baseType))
			{
				knownTypesByBaseType[baseType].UnionWith(derivedTypes);
			}
			else
			{
				knownTypesByBaseType[baseType] = new HashSet<Type>(derivedTypes);
			}
		}

		private static List<Type> GetDerivedTypesOf<T>(IEnumerable<Type> types)
		{
			Type baseType = typeof(T);
			return types.Where(t => t.IsSubclassOf(baseType)).ToList();
		}

		private static string GetMethodInfoKey(MethodInfo methodInfo)
		{
			return methodInfo.ReflectedType.Name + "." + methodInfo.Name;
		}
	}
}